#include "common.h"
#include "counter.h"
#include "trace.h"
#include "Vfrontend_test.h"
#include <queue>
#define MAX_WAVE 100000
#define TRACE 1
#define LOG_DEBUG 0
#define RUN_CYCLE 4000000
#define BLOCK_SIZE 2
enum
{
    NONE = 0,
    Branch,
    Jump,
    Call,
    Ret,
    CallRet
};
typedef struct
{
    bool valid;
    bool taken;
    word_t target;
    uint8_t type;

    uint8_t btb_sc;
    bool ras_valid;
    uint32_t ras_pointer;
    word_t ras_entry;
} BPU_Res;
typedef struct
{
    OP *op;
    bool bpred_miss;
    BPU_Res bpu_res;
} FrontendOP;
std::queue<std::tuple<FrontendOP, word_t>> ops;
word_t pc = 0x80000000ull;
bool wrong_path = false;
word_t correct_pc;

Vfrontend_test top;
VerilatedFstC *tfp;
static uint64_t dump_time = 0;
void dump()
{
#if TRACE
    if (dump_time < MAX_WAVE)
    {
        tfp->dump(dump_time++);
    }
#endif
}
void run()
{
    top.clk = 0;
    top.eval();
    dump();
    top.clk = 1;
    top.eval();
    dump();
}

void reset()
{
    top.rst = 1;
    run();
    run();
    run();
    top.rst = 0;
}

void execute_bru_op(FrontendOP &fop)
{

    bool taken = fop.op->trace.branch_taken;
    word_t addr = fop.op->trace.pc;
    word_t target = fop.op->trace.branch_pc;
    uint8_t type = NONE;
    auto trace = fop.op->trace;
    auto opcode = fop.op->trace.opcode;
    if (opcode == OP_CBR)
    {
        type = Branch;
    }
    else if (opcode == OP_JMP)
    {
        int8_t rs1 = trace.src[0];
        int8_t rd = trace.dst;
        bool rs1_link = rs1 == 1 || rs1 == 5;
        bool rd_link = rd == 1 || rd == 5;
        bool jal = rs1 == -1;
        type = Jump;
        if (jal && rd_link)
        {
            type = Call;
        }
        else
        {
            if (!rd_link && rs1_link)
            {
                type = Ret;
            }
            else if (rd_link && !rs1_link)
            {
                type = Call;
            }
            else if (rd_link && rs1_link && rd != rs1)
            {
                type = CallRet;
            }
            else if (rd_link && rs1_link && rd == rs1)
            {
                type = Call;
            }
        }
    }

    BPU_Res correct_res = {0};
    if (fop.bpred_miss || type != NONE)
    {
        if (fop.bpu_res.valid)
        {
            correct_res = fop.bpu_res;
        }
        else
        {
            switch (type)
            {
            case Jump:
                break;
            case Branch:
                correct_res.btb_sc = taken ? 2 : 1;
                break;
            case Call:
            case Ret:
            case CallRet:
                // LOG("Update pc 0x%08lx target 0x%08lx type %d index %lu at %lu", addr, target, type, (addr - 0x80000000) / 4, dump_time);
                break;
            default:
                LOG("[ERROR] should not update NONE branch instruction to frontend");
                break;
            }
        }
        correct_res.taken = taken;
        correct_res.target = target;
        correct_res.valid = true;
        correct_res.type = type;
    }
    if (fop.bpred_miss)
    {
        // frontend->redirect(addr, correct_res);
        pc = correct_res.taken ? correct_res.target : addr + 4;
        wrong_path = false;
        if (correct_pc != pc)
        {
            LOG("[ERROR] redirect target not correct!");
            return;
        }
    }
    else if (type != NONE)
    {
        correct_res.ras_valid = false;
        // frontend->update(addr, correct_res);
    }
    if (fop.bpred_miss || type != NONE)
    {
    }
#if LOG_DEBUG
    LOG("Update pc 0x%08lx target 0x%08lx type %d index %lu at %lu", addr, correct_res.target, correct_res.type, (addr - 0x80000000) / 4, dump_time);
#endif
    top.i_redirect_pc = addr;
    top.i_redirect_valid = correct_res.valid;
    top.i_redirect_bpu_res_branch_type = correct_res.type;
    top.i_redirect_bpu_res_taken = correct_res.taken;
    top.i_redirect_bpu_res_ras_valid = correct_res.ras_valid;
    top.i_redirect_bpu_res_ras_pointer = correct_res.ras_pointer;
    top.i_redirect_bpu_res_bi_cnt = correct_res.btb_sc;
    top.i_redirect_bpu_res_target = correct_res.target;
    top.i_redirect_bpu_res_ras_top = correct_res.ras_entry;
}

int main()
{
    Counter op_cnt("op_cnt");
    Counter cycle("cycle");
#if TRACE
    Verilated::traceEverOn(true);
    tfp = new VerilatedFstC();
    top.trace(tfp, 0);
    tfp->open("Vfrontend_test.fst");
#endif
    init_trace(std::string("./trace/op.trace"));
    while (!ops.empty())
    {
        ops.pop();
    }
    uint64_t test_cycle = RUN_CYCLE;
    top.i_redirect_valid = 0;
    bool finish = false;
    reset();
    while (cycle < test_cycle && !finish)
    {
        if (cycle % 65536 == 0)
        {

#if !LOG_DEBUG
            LOG("Running cycle : %lu", (uint64_t)cycle);
#endif
        }
        top.i_redirect_valid = 0;

        if (!ops.empty())
        {
            auto opf = ops.front();
            auto fop = std::get<0>(opf);
            auto pcycle = std::get<1>(opf);
            if (pcycle + 4 < cycle)
            {
                execute_bru_op(fop);
                free_op(fop.op);
                ops.pop();
            }
        }
        cycle++;
        run();
        uint32_t r1 = rand();
        uint32_t r2 = r1 / 100;
        r1 %= 100;
        r2 %= 100;
        top.o_iram_ready = r1 < 95;
        top.o_ibuf_ready = r2 < 80;
        // top.o_iram_ready = 1;
        // top.o_ibuf_ready = 1;
        if (top.o_ibuf_valid & top.o_ibuf_ready)
        {
            BPU_Res bpu_res[BLOCK_SIZE] = {0};
            size_t num_to_fetch = 0;
            for (size_t i = 0; i < BLOCK_SIZE; i++)
            {
                if (top.o_ibuf_entry_valid[i] == 0)
                {
                    continue;
                }
                num_to_fetch = i + 1;
                if (top.o_ibuf_entry_bpu_res_valid[i])
                {
                    bpu_res[i].valid = 1;
                    bpu_res[i].taken = top.o_ibuf_entry_bpu_res_taken[i];
                    bpu_res[i].target = top.o_ibuf_entry_bpu_res_target[i];
                    bpu_res[i].type = top.o_ibuf_entry_bpu_res_branch_type[i];
                    bpu_res[i].btb_sc = top.o_ibuf_entry_bpu_res_bi_cnt[i];
                    bpu_res[i].ras_valid = top.o_ibuf_entry_bpu_res_ras_valid[i];
                    bpu_res[i].ras_pointer = top.o_ibuf_entry_bpu_res_ras_pointer[i];
                    bpu_res[i].ras_entry = top.o_ibuf_entry_bpu_res_ras_top[i];
                }
                pc = top.o_ibuf_entry_pc[i];

                if (wrong_path)
                {
                    continue;
                }

                auto op = read_op();
                op_cnt++;
                if (op->valid == false)
                {
                    free_op(op);
                    if (ops.empty())
                    {
                        finish = true;
                        break;
                    }
                    word_t dnpc = op->trace.branch_taken ? op->trace.branch_pc : (op->trace.pc + 4);
                    continue;
                }
                if (op->trace.pc != pc)
                {
                    LOG(COLOR("[Frontend] read op %lu pc 0x%08lx at %lu in wrong path.", RED), op->id, pc, dump_time);
                    finish = true;
                }
                else
                {
#if LOG_DEBUG
                    LOG(COLOR("[Frontend] read op %lu pc 0x%08lx at %lu.", GREEN), op->id, pc, dump_time);
#endif
                }
                word_t dnpc = op->trace.branch_taken ? op->trace.branch_pc : (op->trace.pc + 4);
                word_t pnpc = pc + 4;
                if (bpu_res[i].valid && bpu_res[i].taken)
                {
                    pnpc = bpu_res[i].target;
                }

                bool bpred_hit = dnpc == pnpc;

                if (bpred_hit)
                {
                    free_op(op);
                }
                else
                {
                    wrong_path = true;
                    correct_pc = dnpc;
                    FrontendOP fop;
                    fop.op = op;
                    fop.bpred_miss = !bpred_hit;
                    fop.bpu_res = bpu_res[i];
                    ops.push({fop, cycle});
                }
            }
        }
    }

    run();
    run();
#if TRACE
    tfp->close();
    delete tfp;
#endif
    LOG("Average Fetch Speed %.03f instructions per cycle", (float)op_cnt / cycle);
    Counter::print_counters();
    return 0;
}